/*
 *
 * Copyright (C) 2012 Walter Lütgenau
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */
package offset.nodes.client.xpath.model;

import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.jcr.PropertyType;
import javax.swing.tree.DefaultMutableTreeNode;
import offset.nodes.client.model.TreeNodeUserObject;

/**
 *
 * @author Walter Luetgenau
 */
public class XPathQueryReader {

    String typeConstraint;
    String pathConstraint = "//";
    DefaultMutableTreeNode valueConstraint = new DefaultMutableTreeNode();
    String[] columnSpecifier;
    SortColumn[] orderingSpecifier = new SortColumn[0];
    static final String ELEMENT_FUNCTION = "element(*,";
    static final String ANY_PATH = "//";
    static final String ORDER_BY = "order by";

    class TokenInfo {

        String value;
        int type;
        boolean regularExpression;

        public TokenInfo(String value, int type) {
            this.value = value;
            this.type = type;
            this.regularExpression = false;
        }

        public TokenInfo(String value, int type, boolean regularExpression) {
            this.value = value;
            this.type = type;
            this.regularExpression = regularExpression;
        }

        public int getType() {
            return type;
        }

        public String getValue() {
            return value;
        }

        public boolean isRegularExpression() {
            return regularExpression;
        }
    }

    class Token {

        int start;
        int end;
        String value;
        int type;

        public Token(int start, int end, String value, int type) {
            this.start = start;
            this.end = end;
            this.value = value;
            this.type = type;
        }

        public int getStart() {
            return start;
        }

        public int getEnd() {
            return end;
        }

        public int getType() {
            return type;
        }

        public String getValue() {
            return value;
        }
    }

    class Tokenizer {

        TokenInfo[] tokenInfos;
        Matcher[] matchers;
        int[] starts;
        String input;
        int pos = 0;
        final int EMPTY = -1;
        final int NOT_FOUND = -2;

        public Tokenizer(TokenInfo[] inputTokens) {
            this.tokenInfos = inputTokens;
        }

        public Tokenizer(TokenInfo[] inputTokens, String input) {
            this.tokenInfos = inputTokens;
            this.input = input;
        }

        public void setInput(String input) {
            this.input = input;
            this.pos = 0;
            matchers = new Matcher[tokenInfos.length];
            starts = new int[tokenInfos.length];
            for (int i = 0; i < tokenInfos.length; i++) {
                if (tokenInfos[i].isRegularExpression()) {
                    Pattern pattern = Pattern.compile(tokenInfos[i].getValue());
                    matchers[i] = pattern.matcher(input);
                } else
                    matchers[i] = null;
                starts[i] = EMPTY;
            }
        }

        public Token nextToken() {
            if (input == null)
                return null;

            while (pos < input.length()) {
                for (int i = 0; i < tokenInfos.length; i++) {
                    TokenInfo tokenInfo = tokenInfos[i];

                    if (tokenInfo.isRegularExpression()) {
                        if (starts[i] == EMPTY)
                            if (matchers[i].find(pos))
                                starts[i] = matchers[i].start();
                            else
                                starts[i] = NOT_FOUND;
                        else if (starts[i] == NOT_FOUND)
                            continue;

                        if (starts[i] == pos) {
                            pos = matchers[i].end();
                            int start = matchers[i].start();
                            int end = matchers[i].end();
                            return new Token(start, end, input.substring(start, end), tokenInfo.getType());
                        }
                    } else if (input.substring(pos).startsWith(tokenInfo.getValue())) {
                        int start = pos;
                        int end = pos + tokenInfo.getValue().length();
                        pos += tokenInfo.getValue().length();
                        return new Token(start, end, input.substring(start, end), tokenInfo.getType());
                    }
                }

                pos++;
            }

            return null;
        }

        public void setPos(int pos) {
            this.pos = pos;
            for (int i = 0; i < tokenInfos.length; i++) {
                if (matchers[i] != null)
                    matchers[i].reset();
                starts[i] = EMPTY;
            }
        }
    }

    public void read(String query) {
        int startElement = query.indexOf(ELEMENT_FUNCTION);
        if (startElement < 0)
            return;

        // path constraint
        pathConstraint = query.substring(0, startElement);
        if (pathConstraint.equals(ANY_PATH))
            pathConstraint = "";
        else
            pathConstraint = pathConstraint.substring(0, pathConstraint.length() - 2);

        // type constraint
        int typeStart = startElement + ELEMENT_FUNCTION.length();
        int typeEnd = query.indexOf(")", typeStart);
        typeConstraint = query.substring(typeStart, typeEnd).trim();

        int parsed = typeEnd + 1;
        int valueConstraintStart = query.indexOf("[", typeEnd);
        if (valueConstraintStart >= 0) {
            int valueConstraintEnd = query.indexOf("]", valueConstraintStart);
            parseValueConstraint(query.substring(valueConstraintStart + 1, valueConstraintEnd));
            parsed = valueConstraintEnd;
        } else {
            valueConstraint = new DefaultMutableTreeNode(new AndNode());
        }

        int columnSpecifierStart = query.indexOf("(", parsed);
        if (columnSpecifierStart >= 0) {
            int columnSpecifierEnd = query.indexOf(")", columnSpecifierStart);
            parseColumnSpecifier(query.substring(columnSpecifierStart + 1, columnSpecifierEnd));
            parsed = columnSpecifierEnd;
        }

        int orderSpecifierStart = query.indexOf(ORDER_BY);
        if (orderSpecifierStart >= 0)
            parseOrderSpecifier(query.substring(orderSpecifierStart + ORDER_BY.length()));
    }

    protected int parseExpression(String filter, DefaultMutableTreeNode expression) {
        final int T_EQUAL = 1;
        final int T_GREATER = 2;
        final int T_LESS = 3;
        final int T_NOT_EQUAL = 4;
        final int T_GREATER_EQUAL = 5;
        final int T_LESS_EQUAL = 6;
        final int T_END = 7;
        final int T_NAME = 8;
        final int T_WHITESPACE = 9;
        final int T_STRING_LITERAL = 10;
        final int T_NUMERIC_LITERAL = 11;

        TokenInfo[] sequenceTokens = new TokenInfo[]{
            new TokenInfo("=", T_EQUAL),
            new TokenInfo(">", T_GREATER),
            new TokenInfo("<", T_LESS),
            new TokenInfo("!=", T_NOT_EQUAL),
            new TokenInfo(">=", T_GREATER_EQUAL),
            new TokenInfo("<=", T_LESS_EQUAL),
            new TokenInfo("[ \\t] ", T_WHITESPACE, true),
            new TokenInfo("'[a-zA-Z0-9!\"$%&/()=+\\-.,;:_]+'", T_STRING_LITERAL, true),
            new TokenInfo("[0-9.-]+", T_NUMERIC_LITERAL, true),
            new TokenInfo("and|or|\\)|\\(", T_END, true),
            new TokenInfo("[a-zA-Z0-9:]+", T_NAME, true)
        };

        Tokenizer tokenizer = new Tokenizer(sequenceTokens);
        tokenizer.setInput(filter);

        int pos = 0;
        ExpressionNode userObject = ExpressionNode.createEmpyExpression();
        expression.setUserObject(userObject);

        while (pos < filter.length()) {
            Token token = tokenizer.nextToken();
            if (token == null)
                break;

            pos = token.getEnd();

            switch (token.getType()) {
                case T_END:
                    return token.getStart();

                case T_NAME:
                    userObject.setPropertyName(token.getValue());
                    break;

                case T_STRING_LITERAL:
                    userObject.setValue(token.getValue().substring(1, token.getValue().length() - 1));
                    userObject.setPropertyType(PropertyType.STRING);
                    tokenizer.setPos(pos);
                    break;

                case T_NUMERIC_LITERAL:
                    userObject.setValue(token.getValue());
                    userObject.setPropertyType(PropertyType.LONG);
                    tokenizer.setPos(pos);
                    break;

                case T_EQUAL:
                    userObject.setOperator(Operator.equal);
                    break;

                case T_LESS:
                    userObject.setOperator(Operator.less);
                    break;

                case T_GREATER:
                    userObject.setOperator(Operator.greater);
                    break;

                case T_GREATER_EQUAL:
                    userObject.setOperator(Operator.greaterEqual);
                    break;

                case T_LESS_EQUAL:
                    userObject.setOperator(Operator.lessEqual);
                    break;

                case T_NOT_EQUAL:
                    userObject.setOperator(Operator.notEqual);
                    break;
            }
        }

        return pos;
    }

    protected int parseMethodExpression(String filter, DefaultMutableTreeNode expression, Operator operator) {
        final int T_CHILD = 1;
        final int T_PROPERTY = 2;
        final int T_END = 3;
        final int T_WHITESPACE = 4;
        final int T_STRING_LITERAL = 5;

        TokenInfo[] sequenceTokens = new TokenInfo[]{
            new TokenInfo("[a-zA-Z0-9:]+/", T_CHILD, true),
            new TokenInfo("[a-zA-Z0-9:]+,", T_PROPERTY, true),
            new TokenInfo("[ \\t(] ", T_WHITESPACE, true),
            new TokenInfo("'[a-zA-Z0-9!\"$%&/()=+\\-.,;:_ ]+'", T_STRING_LITERAL, true),
            new TokenInfo(")", T_END)
        };

        Tokenizer tokenizer = new Tokenizer(sequenceTokens);
        tokenizer.setInput(filter);

        int pos = 0;
        ExpressionNode userObject = ExpressionNode.createEmpyExpression();
        userObject.setOperator(operator);
        expression.setUserObject(userObject);

        while (pos < filter.length()) {
            Token token = tokenizer.nextToken();
            if (token == null)
                break;

            pos = token.getEnd();

            switch (token.getType()) {
                case T_END:
                    return token.getEnd();

                case T_CHILD:
                    userObject.setChildName(token.getValue().substring(0, token.getValue().length() - 1));
                    break;

                case T_PROPERTY:
                    userObject.setPropertyName(token.getValue().substring(0, token.getValue().length() - 1));
                    break;

                case T_STRING_LITERAL:
                    userObject.setValue(token.getValue().substring(1, token.getValue().length() - 1));
                    break;
            }
        }

        return pos;
    }

    protected TreeNodeUserObject parseLogicalOperator(String operator) {
        if (operator.trim().length() == 0)
            return null;

        TreeNodeUserObject userObject = null;
        if (operator.trim().toLowerCase().equals("and"))
            userObject = new AndNode();
        else
            userObject = new OrNode();

        return userObject;
    }

    protected int parseSequence(String filter, DefaultMutableTreeNode node) {
        final int T_ATTRIBUTE = 1;
        final int T_LIKE = 2;
        final int T_CONTAINS = 3;
        final int T_START_SEQUENCE = 4;
        final int T_END_SEQUENCE = 5;
        final int T_OPERATOR_OR = 6;
        final int T_OPERATOR_AND = 7;
        final int T_WHITESPACE = 8;

        TokenInfo[] sequenceTokens = new TokenInfo[]{
            new TokenInfo("@", T_ATTRIBUTE),
            new TokenInfo("jcr:like", T_LIKE),
            new TokenInfo("jcr:contains", T_CONTAINS),
            new TokenInfo("(", T_START_SEQUENCE),
            new TokenInfo(")", T_END_SEQUENCE),
            new TokenInfo("or", T_OPERATOR_OR),
            new TokenInfo("and", T_OPERATOR_AND),
            new TokenInfo(" ", T_WHITESPACE),
            new TokenInfo("\t", T_WHITESPACE)
        };

        Tokenizer tokenizer = new Tokenizer(sequenceTokens);
        tokenizer.setInput(filter);

        int pos = 0;

        TreeNodeUserObject userObject = null;
        DefaultMutableTreeNode child = new DefaultMutableTreeNode();

        while (pos < filter.length()) {
            Token token = tokenizer.nextToken();
            if (token == null)
                break;

            pos = token.getEnd();

            switch (token.getType()) {
                case T_START_SEQUENCE:
                    child = new DefaultMutableTreeNode();
                    node.add(child);
                    pos = parseSequence(filter.substring(token.getEnd()), child);
                    tokenizer.setPos(token.getStart() + pos);
                    break;

                case T_END_SEQUENCE:
                    node.setUserObject(userObject);
                    return pos;

                case T_ATTRIBUTE:
                    child = new DefaultMutableTreeNode();
                    node.add(child);
                    pos = parseExpression(filter.substring(token.getEnd()), child);
                    tokenizer.setPos(token.getEnd() + pos);
                    break;

                case T_LIKE:
                    child = new DefaultMutableTreeNode();
                    node.add(child);
                    pos = parseMethodExpression(filter.substring(token.getEnd()), child, Operator.like);
                    tokenizer.setPos(token.getEnd() + pos);
                    break;

                case T_CONTAINS:
                    child = new DefaultMutableTreeNode();
                    node.add(child);
                    pos = parseMethodExpression(filter.substring(token.getEnd()), child, Operator.contains);
                    tokenizer.setPos(token.getEnd() + pos);
                    break;

                case T_OPERATOR_AND:
                    userObject = new AndNode();
                    break;

                case T_OPERATOR_OR:
                    userObject = new OrNode();
                    break;
            }
        }

        return pos + 1; // because of ignoring the first character
    }

    protected void parseValueConstraint(String filter) {
        int parsed = 0;

        valueConstraint = new DefaultMutableTreeNode();
        valueConstraint.setUserObject(new AndNode());
        DefaultMutableTreeNode sequence = new DefaultMutableTreeNode();

        parseSequence(filter, valueConstraint);
    }

    protected void parseColumnSpecifier(String columnString) {
        columnSpecifier = columnString.split("\\|");
        for (int i = 0; i < columnSpecifier.length; i++) {
            columnSpecifier[i] = columnSpecifier[i].trim().substring(1);
        }
    }

    protected void parseOrderSpecifier(String sortString) {
        String[] sortComponents = sortString.split(",");
        orderingSpecifier = new SortColumn[sortComponents.length];

        for (int i = 0; i < sortComponents.length; i++) {
            String sortComponent = sortComponents[i].trim();
            String property = sortComponent.substring(1, sortComponent.indexOf(" "));
            String direction = sortComponent.substring(sortComponent.lastIndexOf(" ") + 1);

            orderingSpecifier[i] = new SortColumn(property, direction.toLowerCase().equals("ascending") ? SortDirection.ascending : SortDirection.descending);
        }
    }

    public String[] getColumnSpecifier() {
        return columnSpecifier;
    }

    public SortColumn[] getOrderingSpecifier() {
        return orderingSpecifier;
    }

    public String getPathConstraint() {
        return pathConstraint;
    }

    public String getTypeConstraint() {
        return typeConstraint;
    }

    public DefaultMutableTreeNode getValueConstraint() {
        return valueConstraint;
    }

    public static void main(String[] arguments) {
        String[] queries = new String[]{
            "/templates/tableTemplate//element(*, nt:childNodeDefinition) [(@jcr:onParentVersion = 'x' and @jcr:requiredPrimaryTypes = 'y' and (@a = 1 or @b > 2))]/(@jcr:onParentVersion | @jcr:requiredPrimaryTypes | @jcr:defaultPrimaryType | @jcr:name | @jcr:path) order by @jcr:name descending, @jcr:path ascending",
            "//element(*, nodes:task) [@nodes:priority = 'HIGH']/(@jcr:path)",
            "//element(*, nt:childNodeDefinition) [(@jcr:onParentVersion = 'x' and @jcr:requiredPrimaryTypes = 'x' and @abc:def = 'y' and (@a = 1 or @b > 2))]/(@jcr:onParentVersion | @jcr:requiredPrimaryTypes | @jcr:defaultPrimaryType | @jcr:name | @jcr:path) order by @jcr:name descending, @jcr:path ascending",
            "/templates/tableTemplate//element(*, nt:childNodeDefinition) [(@jcr:onParentVersion = 'x' and @jcr:requiredPrimaryTypes = 'x')] order by @jcr:name descending, @jcr:path ascending"
        };

        for (String query : queries) {
            XPathQueryReader reader = new XPathQueryReader();
            reader.read(query);
            XPathQueryWriter writer = new XPathQueryWriter();
            writer.setPathConstraint(reader.getPathConstraint());
            writer.setTypeConstraint(reader.getTypeConstraint());
            writer.setColumnSpecifier(reader.getColumnSpecifier());
            writer.setOrderingSpecifier(reader.getOrderingSpecifier());
            writer.setValueConstraint(reader.getValueConstraint());

            boolean ok = writer.getXPathQuery().equals(query);
            System.out.println(ok);

            if (!ok) {
                System.out.println(query);
                System.out.println(writer.getXPathQuery());
            }
        }
   }
}
